#include<ctype.h>
#include<cstring>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include <unistd.h>
#include <sys/types.h>                                             
#include <sys/wait.h>
#include<unordered_map>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
//1.命令行参数表
#define MAXARGC 128 
char* g_argv[MAXARGC];
int g_argc=0;

//2.环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs=0;

//3.别名映射表
std::unordered_map<std::string,std::string>alias_list;//取别名

//关于重定向，我们的核心内容是
#define NONE_REDIR 0  //是否重定向
#define INPUT_REDIR 1  //输入
#define OUTPUT_REDIR 2  //输出
#define APPEND_REDIR 3  //追加

int redir=NONE_REDIR;
std::string filename;


//for test
char cwd[1024];
char cwdenv[1024];

//last exit code 
int lastcode=0;
const char *GetUserName()//用户名
{
  const char*name=getenv("USER");
  return name==NULL?"None":name;
}
const char *GetHostName()//主机名
{
  const char*hostname=getenv("HOSTNAME");
  return hostname==NULL?"None":hostname;
}
const char *GetPwd()//获取当前文件位置
{
  //const char*pwd=getenv("PWD");
  const char*pwd=getcwd(cwd,sizeof(cwd));//获得当前进程的工作路径
  if(pwd!=NULL)//如果不是空的话，那么我们就获得了环境变量了
  {
    snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd);//进行一个格式化操作
    putenv(cwdenv);//谁调用putenv,就在谁的上下文当中导出环境变量，那么我们就将当前工作路径的环境变量导出来了
    
  }
  return pwd==NULL?"None":pwd;
}

const char*GetHome()
{
    //环境变量中有个HOME环境变量里面存的就是家目录
    const char *home=getenv("HOME");
    return home==NULL?"":home;
}


void InitEnv()
{
    extern char**environ;
    memset(g_env,0,sizeof(g_env));
    g_envs=0;

    //本来要从配置文件来，现在我们直接从父shell来
    //1.获取环境变量
    for(int i=0;environ[i];i++)//父shell所有的环境变量
    {
      //1.1申请空间
        g_env[i]=(char*)malloc(strlen(environ[i])+1);//开辟一个空间
        strcpy(g_env[i],environ[i]);//直接将后者的信息拷贝到前者里面去
        g_envs++;
        //那么循环结束之后，环境变量表的都从父进程拷贝到子进程里面去了
        
    }
    
    g_env[g_envs]=NULL;//最后一定得是空
    //2.将获取的环境变量中都要导入到环境变量中
    for(int i=0;g_env[i];i++)
    {
      putenv(g_env[i]);//将环境变量导入到地址空间中·
    }
    environ=g_env;//c语言提供的环境变量就指向了父进程的环境变量表了
}
bool  cd()
{
        //如果遇到的是内键命令的话，我们直接让bash执行这个命令
        //我们这里的话使用chdir，哪个进程带调用dir就修改工作路径
        if(g_argc==1)//这里的话就是cd也占了一个命令，
        {
            std::string home=GetHome();
            if(home.empty())return true;//如果这个home是空的话，就说明上面返回失败了，那么我们就执行完了内键命令了，直接返回true就行了
            chdir(home.c_str());//执行cd命令，那就是进入到家目录
            //就是直接改变工作路径了，相当于变相的执行了cd这个命令了 ，改变了这个目录了
        }
        else//就是说明argc>0，里面的命令个数是大于0的 
        {
            std::string where=g_argv[1];//现在我知道要去哪里了
            if(where=="-")
            {
                //不做了
            }
            else if(where=="~")
            {
                //不做了
            }
            else 
            {
                chdir(where.c_str());//就是直接切换到对应的路径上去了
            }
        }
        return true;
}
void Echo()
{
      if(g_argc==2)//得有两个命令行参数，就是echo后面得跟命令参数
      {
        
          //echo "hello world"
          //echo $?
          //echo $PATH
          std::string opt = g_argv[1];
          if(opt=="$?")
          {
            std::cout<<lastcode<<std::endl;
            lastcode=0;
            
          }
          else if(opt[0]=='$')
          {
            std::string env_name=opt.substr(1);//环境变量的名字
            const char *env_value=getenv(env_name.c_str());
            if(env_value)//如果这个环境变量不为空的话，那么我们将这个环境变量打出来就行了
            {
              std::cout<<env_value<<std::endl;
            }
          }
          else 
          {
            std::cout<<opt<<std::endl;
          }
          
      }

}
std::string DirName(const char*pwd)
{
#define SLASH "/"
    std::string dir=pwd;
    if(dir==SLASH)return SLASH;//说明是根目录
    auto pos=dir.rfind(SLASH);
    if(pos==std::string::npos)return "BUG?";//就是找了半天没有找到斜杠的话，那么我们直接返回bug了
    return dir.substr(pos+1);//否则的话就找到了，直接将子串
}
void MakeCommandLine(char cmd_prompt[],int size)//进行格式化操作
{
  snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());
  //snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
}
void PrintCommandPrompt()//打印命令行提示符
{
  char prompt[COMMAND_SIZE];
  MakeCommandLine(prompt,sizeof(prompt));
  printf("%s",prompt);
  fflush(stdout);
}

bool GetCommandLine(char *out,int size)
{
  char *c=fgets(out,size,stdin);//获取命令行输入,stdin就是标准输入
  if(c==NULL)return false;
  out[strlen(out)-1]=0;//清理\n，就是减少多余的换行操作
  if(strlen(out)==0)return false;//就是用户只输入了一个回车，那么我们直接返回就行了
  return true;
}

bool CommandParse(char *commandline)
{
    //命令行分析 "ls -a -l"->"ls" "-a" "-l"  将命令拆开放到argv数组里面去
#define SEP " " 
  g_argc=0;
  g_argv[g_argc++]=strtok(commandline,SEP);
  
  while((bool)(g_argv[g_argc++]=strtok(NULL,SEP)));
  g_argc--;
  return g_argc>0?true:false;//如果命令行参数的个数是大于0的话，那么我们就返回ture，否则的话就返回false就行了
  
}

void PrintArgv()
{
  for(int i=0;g_argv[i];i++)
  {
    printf("argv[%d]->%s\n",i,g_argv[i]); 
  }
}

bool CheckAndExecuBuiltin()
{

   //如果内键命令做重定向的话，那么我们就得更改shell的标准输入，输出，错误
    std::string cmd=g_argv[0];
  //g_argv[0]就是程序的名字
    if(cmd=="cd")
    {
        cd();
        return true;//是内键命令的话
    }
    else if(cmd=="echo")//ehco也是内键命令
    {
      Echo();
    }
    else if(cmd=="export")//导入环境变量 
    {

    }
    else if(cmd=="alias")//说明我们要取别名了
    {
      //std::string nickname=g_argv[1];
      //alias_list.insert(k,v);
    }
    return false;
}
int Execute()
{
   pid_t id=fork();
   if(id==0)//子进程
   {  
      int fd=-1;
      //子进程检测重定向情况
      if(redir==INPUT_REDIR)//输入重定向
      {
        //访问的文件是一定存在的
        fd=open(filename.c_str(),O_RDONLY);//仅读
        //下面是重定向失败了
        if(fd<0) exit(1); 

        //打开成功的情况
        dup2(fd,0);//从我打开的这个文件里面去读
        //这行代码将标准输入（stdin）重定向到文件描述符 fd，意味着之后的输入操作（例如 scanf() 或 cin）会从这个文件读取数据，而不是从键盘读取。
        close(fd);
      }
      else if(redir==OUTPUT_REDIR)//输出重定向
      {
        fd=open(filename.c_str(),O_CREAT|O_WRONLY |O_TRUNC,0666);// 文件不存在就创建，对文件进行数据的写入的，打开文件时将文件内容清空,文件不存在的话我们是需要创建文件并且给到权限0666
        if(fd<0)exit(2);//表示重定向失败了
        dup2(fd,1);//本来是写到显示器上面的，现在改成写到文件里面去
        close(fd);
      }
      else if(redir==APPEND_REDIR)//追加重定向
      {
        fd=open(filename.c_str(),O_CREAT|O_WRONLY |O_APPEND);// 文件不存在就创建，对文件进行数据的写入的，打开文件是在之前的数据之后进行数据的追加操作的，不清空之前存在的数据的
        if(fd<0)exit(2);//表示重定向失败了
        dup2(fd,1);
        close(fd);

      }
      else//没有重定向 
      {}

      //进程替换是否会影响到重定向的结果吗？不会影响的
      
      

      //child
      execvp(g_argv[0],g_argv);//直接去我们的环境变量中去找这个命令
      exit(1);
    }
    int status=0;
    //father
    pid_t rid=waitpid(id,&status,0);
    if(rid>0)//说明我们是等待成功了的
    {
        lastcode=WEXITSTATUS(status);
    }
    return 0;
}

void TrimSpace(char cmd[],int &end)//去掉空格
{
  while(isspace(cmd[end]))//如果此时的位置是空格的话，那么我们就让end++
  {
    end++;
  }
}

void RedirCheck(char cmd[])
{
  redir=NONE_REDIR;//没有重定向
  filename.clear();//清空内容
  int start=0;
  int end=strlen(cmd)-1;

  //"ls -a -l >file.txt"   >   >>  <
  while(end>start)//我们从后往前去寻找
  {
    if(cmd[end]=='<')
    {
      cmd[end++]=0;//将左边和右边区分开了
      TrimSpace(cmd,end);//去掉空格
      redir=INPUT_REDIR;//输入重定向
      filename=cmd+end;//文件名的开始就是从end位置开始的
      break;
    }
    else if(cmd[end]=='>')
    {
      if(cmd[end-1]=='>')//如果两个位置都是>的情况，那么就是追加重定向了
      {
        //  >>
        cmd[end-1]=0;
        redir=APPEND_REDIR;//输入重定向
      }
      else 
      {
        // >
        
        redir=OUTPUT_REDIR;//输入重定向
      }
        cmd[end++]=0;
        TrimSpace(cmd,end);//去掉空格
        filename=cmd+end;
        break;
    }
    else //没有重定向
    {
      end--;
    }
  }
}


int main()
{



  //shell启动的时候，从系统中获取环境变量
  //我们的环境变量信息应该从父shell统一来
  InitEnv();
  while(true)
  {
    //1.输出命令行提示符
    PrintCommandPrompt(); 
   //2.获取用户输入的命令 
    char commandline[COMMAND_SIZE];
    if(!GetCommandLine(commandline,sizeof(commandline)))//如果我们获取成功了的话，就将用户输入的信息显示出来就行了
    {
      continue;
    }
    //printf("ehco %s\n",commandline);

    //命令行分析 "ls -a -l >file.txt"->"ls -a -l" "file.txt"
    //将左半部分拆成右半部分
    //判定重定向方式
    
    //3.重定向分析
    RedirCheck(commandline);
    
    printf("redir:%d, filename:%s\n",redir,filename.c_str());

    //4.命令行分析 "ls -a -l"->"ls" "-a" "-l"
    if(!CommandParse(commandline))
        continue;//如果解析成功的话就可以进行下面的命令，解析失败的话就不用执行这个命令了
    //PrintArgv();
   
    //可以加上检测别名这一步
    //5.检测并处理内键命令
    if(CheckAndExecuBuiltin())
        continue;//如果是内键名命令的话我们直接跳出本次执行后面的命令了
    //是内键命令，返回了treu的话，那么我们就不需要创建子进程，不需要让我们的子进程去执行这个cd命令啥的
    //不是内键命令的话我们才执行下面的命令
    //6..执行命令
    Execute();
  
  }
  return 0;
}
